Skip to content

Comments

solve: solve 도메인 신설 — 풀이 인증/기록/랭킹 분리 (#160)#163

Merged
ryuwldnjs merged 3 commits intomainfrom
160-refactor-solve-도메인-신설-풀이-인증기록랭킹-분리
Feb 17, 2026

Hidden character warning

The head ref may contain hidden characters: "160-refactor-solve-\ub3c4\uba54\uc778-\uc2e0\uc124-\ud480\uc774-\uc778\uc99d\uae30\ub85d\ub7ad\ud0b9-\ubd84\ub9ac"
Merged

solve: solve 도메인 신설 — 풀이 인증/기록/랭킹 분리 (#160)#163
ryuwldnjs merged 3 commits intomainfrom
160-refactor-solve-도메인-신설-풀이-인증기록랭킹-분리

Conversation

@ryuwldnjs
Copy link
Member

@ryuwldnjs ryuwldnjs commented Feb 17, 2026

  • MemberSolvedProblem 엔티티 + Repository를 solve 패키지로 이동
  • MemberService에서 verifyProblemSolved, getDailySolved 추출 → SolveService
  • RankingService, RankingController를 solve 패키지로 이동
  • SolveController 신규 생성, 랭킹 엔드포인트 통합
  • 기존 API 엔드포인트 @deprecated로 하위 호환 유지
  • GlobalRankingProjection을 별도 인터페이스로 분리
  • MemberSolvedSummaryProjection을 solve/dto/projection으로 이동
  • SecurityConfig에 /api/solve/ranking/** permitAll 추가
  • 테스트 이동 및 SolveServiceTest 신규 생성

관련 이슈

변경 내용

변경 유형

  • [FEATURE] 기능 구현
  • [BUG] 버그 수정
  • [REFACTOR] 리팩토링
  • [CHORE] 설정/의존성
  • [DOCS] 문서

테스트

  • 테스트 완료

스크린샷 (UI 변경 시)

참고사항

Summary by CodeRabbit

  • New Features
    • /api/solve/* 엔드포인트 추가: 문제 검증, 일별 풀이 통계, 글로벌 랭킹 조회 제공.
  • Refactor
    • 풀이·랭킹 관련 기능을 전용 모듈로 재구성하여 API 경로 및 내부 모듈 정리.
  • Deprecated
    • 기존 랭킹/일별 풀이 엔드포인트는 deprecated 표기 및 새 위치로 이전 권장.
  • Security
    • 일부 랭킹 경로에 대해 인증 예외 허용(프론트마이그레이션 후 변경 예정).
  • Tests
    • SolveService 집중 테스트 추가 및 일별 집계·경계값 검증 강화.

- MemberSolvedProblem 엔티티 + Repository를 solve 패키지로 이동
- MemberService에서 verifyProblemSolved, getDailySolved 추출 → SolveService
- RankingService, RankingController를 solve 패키지로 이동
- SolveController 신규 생성, 랭킹 엔드포인트 통합
- 기존 API 엔드포인트 @deprecated로 하위 호환 유지
- GlobalRankingProjection을 별도 인터페이스로 분리
- MemberSolvedSummaryProjection을 solve/dto/projection으로 이동
- SecurityConfig에 /api/solve/ranking/** permitAll 추가
- 테스트 이동 및 SolveServiceTest 신규 생성
@ryuwldnjs ryuwldnjs linked an issue Feb 17, 2026 that may be closed by this pull request
8 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 17, 2026

Note

.coderabbit.yaml has unrecognized properties

CodeRabbit is using all valid settings from your configuration. Unrecognized properties (listed below) have been ignored and may indicate typos or deprecated fields that can be removed.

⚠️ Parsing warnings (1)
Validation error: Unrecognized key(s) in object: 'path_filters'
⚙️ Configuration instructions
  • Please see the configuration documentation for more information.
  • You can also validate your configuration using the online YAML validator.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Walkthrough

풀이(검증·기록·통계·랭킹)를 member 도메인에서 분리하여 새 solve 도메인으로 이동·구현하고, Member 쪽에는 해당 책임을 위임·deprecated 처리했습니다.

Changes

Cohort / File(s) Summary
보안 및 라우팅 설정
src/main/java/com/ryu/studyhelper/config/security/SecurityConfig.java
/api/solve/ranking/** 경로를 인증 예외로 추가(프론트 마이그레이션 TODO).
Member 컨트롤러 변경
src/main/java/com/ryu/studyhelper/member/MemberController.java
풀이 관련 엔드포인트를 deprecated 래퍼로 유지하고 SolveService로 위임하도록 변경.
Member 서비스 축소
src/main/java/com/ryu/studyhelper/member/MemberService.java
Problem/MemberSolvedProblem 관련 리포지토리·메서드 제거. 풀이 수 집계는 solveService.countByMemberId로 대체.
새로운 Solve 컨트롤러
src/main/java/com/ryu/studyhelper/solve/controller/SolveController.java
/api/solve 하위에 verifyProblemSolved, getDailySolved, getGlobalRanking 엔드포인트 추가(인증·레이트리밋·Swagger 주석 포함).
새로운 Solve 서비스
src/main/java/com/ryu/studyhelper/solve/service/SolveService.java
풀이 인증(외부 SolvedAc 호출), 일별 집계(06:00 기준), 전체 카운트, 예외 처리 및 영속화 로직 추가(트랜잭션 포함).
도메인·리포지토리 이동
src/main/java/com/ryu/studyhelper/solve/domain/MemberSolvedProblem.java, src/main/java/com/ryu/studyhelper/solve/repository/MemberSolvedProblemRepository.java
엔티티·리포지토리 패키지 member → solve로 이동, 내부 projection 제거하고 외부 projection 타입 사용하도록 import 변경.
랭킹 관련 이동·호환 처리
src/main/java/com/ryu/studyhelper/solve/service/RankingService.java, src/main/java/com/ryu/studyhelper/solve/controller/RankingController.java
RankingService·컨트롤러를 solve로 이동. 기존 컨트롤러와 메서드에 @Deprecated 주석 추가(새 경로 안내).
DTO·프로젝션 재배치
src/main/java/com/ryu/studyhelper/solve/dto/...
DailySolvedResponse, GlobalRankingResponse, GlobalRankingProjection, MemberSolvedSummaryProjection 등 DTO/프로젝션 패키지 변경.
소비자 코드 import 업데이트
src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java
이동된 타입에 맞춰 import 경로만 업데이트(로직 불변).
테스트 보강·조정
src/test/java/...
MemberServiceTest에서 풀이 관련 테스트 제거·Mock 변경, RankingServiceTest 패키지 수정, SolveServiceTest 신규 추가(검증, 일별 집계, 범위 검증 등).

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SolveController as "SolveController\n(api)"
    participant SolveService as "SolveService\n(service)"
    participant MemberRepo as "MemberRepository\n(db)"
    participant ProblemRepo as "ProblemRepository\n(db)"
    participant MemberSolvedRepo as "MemberSolvedProblemRepository\n(db)"
    participant SolvedAc as "SolvedAcClient\n(external)"

    Client->>SolveController: POST /api/solve/{problemId}/verify-solved (auth)
    SolveController->>SolveService: verifyProblemSolved(memberId, problemId)
    SolveService->>MemberRepo: findById(memberId)
    alt member not found
        MemberRepo-->>SolveService: not found -> throw MEMBER_NOT_FOUND
    else member found
        SolveService->>ProblemRepo: findById(problemId)
        alt problem not found
            ProblemRepo-->>SolveService: not found -> throw PROBLEM_NOT_FOUND
        else problem found
            SolveService->>MemberSolvedRepo: existsByMemberIdAndProblemId?
            alt already solved
                MemberSolvedRepo-->>SolveService: true -> throw ALREADY_SOLVED
            else not solved
                SolveService->>SolvedAc: checkSolved(handle, problemId)
                SolvedAc-->>SolveService: solved? (true/false)
                alt solved
                    SolveService->>MemberSolvedRepo: save(new MemberSolvedProblem)
                    MemberSolvedRepo-->>SolveService: saved
                else not solved
                    SolvedAc-->>SolveService: false -> throw PROBLEM_NOT_SOLVED_YET
                end
            end
        end
    end
    SolveService-->>SolveController: success
    SolveController-->>Client: 200 OK (ApiResponse SUCCESS)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐰 풀잎 위에 발자국 남기며,
새 길로 옮긴 풀이의 기록들,
여섯 시의 하루가 조용히 모여,
랭킹은 새로운 둥지로 흩어지고,
토끼는 깡총, 변화를 축하하네 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.83% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed 제목이 PR의 주요 변경사항을 명확하게 설명하고 있습니다. 'solve 도메인 신설'과 '풀이 인증/기록/랭킹 분리'는 변경사항의 핵심을 정확하게 요약합니다.
Linked Issues check ✅ Passed PR의 코드 변경사항이 linked issue #160의 모든 주요 요구사항을 충족합니다: solve 도메인 신설, MemberSolvedProblem 이동, SolveService 생성, RankingService 이동, SolveController 생성, 관련 DTO 이동, 테스트 추가.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #160 이슈의 범위 내에 있습니다. SecurityConfig의 /api/solve/ranking/** 추가, 기존 엔드포인트 @Deprecated 표시, TeamActivityService 임포트 업데이트 등 모두 도메인 분리와 관련된 필수 변경입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch 160-refactor-solve-도메인-신설-풀이-인증기록랭킹-분리

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions
Copy link

github-actions bot commented Feb 17, 2026

🧪 테스트 결과

145 tests   143 ✅  3s ⏱️
 30 suites    2 💤
 30 files      0 ❌

Results for commit f7cece4.

♻️ This comment has been updated with latest results.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
src/main/java/com/ryu/studyhelper/solve/service/SolveService.java (2)

81-135: getDailySolved 로직이 깔끔하게 구성되어 있습니다.

LinkedHashMap으로 날짜 순서를 보장하고, 빈 날짜를 먼저 초기화하여 모든 날짜가 응답에 포함되도록 하는 설계가 좋습니다.

한 가지 사소한 개선 제안: Line 49의 member.getHandle().isEmpty() 대신 isBlank()를 사용하면 공백 문자만 있는 핸들도 방어할 수 있습니다.

♻️ 제안
-        if (member.getHandle() == null || member.getHandle().isEmpty()) {
+        if (member.getHandle() == null || member.getHandle().isBlank()) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ryu/studyhelper/solve/service/SolveService.java` around
lines 81 - 135, Replace usages of member.getHandle().isEmpty() with
member.getHandle().isBlank() to also treat strings containing only whitespace as
empty; locate the check (currently using isEmpty) where member handles are
validated and update it to isBlank(), ensuring any logic that depends on an
empty handle (e.g., in getDailySolved-related user formatting or display flows)
continues to behave the same after the change.

147-156: getAdjustedDateTeamActivityService.getMissionDate와 중복됩니다.

두 메서드 모두 "오전 6시 기준 날짜 보정" 로직을 구현하고 있습니다. 현재는 기능적으로 동일하지만 독립적으로 관리되므로 향후 한쪽만 수정될 위험이 있습니다. 추후 공통 유틸리티 클래스로 추출하는 것을 검토해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ryu/studyhelper/solve/service/SolveService.java` around
lines 147 - 156, getAdjustedDate duplicates logic in
TeamActivityService.getMissionDate; extract the shared "06:00 기준 날짜 보정" into a
single utility method (e.g., MissionDateUtils.adjustToMissionDate(LocalDateTime)
or DateUtils.getMissionDate) and update both SolveService.getAdjustedDate and
TeamActivityService.getMissionDate to call that util (or remove the duplicate
method and replace callers with the util). Ensure the util accepts
LocalDateTime, performs the same hour < 6 -> minusDays(1) rule, and update
imports/usages so both SolveService and TeamActivityService delegate to that
common method.
src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java (1)

154-181: assignRanks 로직이 RankingService와 중복됩니다.

TeamActivityService.assignRanks (Lines 154-181)와 RankingService.assignRanks (Lines 37-64)의 순위 부여 알고리즘이 동일한 패턴을 사용하고 있습니다. 입력/출력 타입(MemberSolvedSummaryProjection vs GlobalRankingProjection)만 다릅니다.

PR 목표에 후속 작업으로 언급되어 있지만, 추후 공통 유틸리티 메서드로 추출하거나 제네릭/함수형 인터페이스를 활용하여 통합하는 것을 권장합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java`
around lines 154 - 181, The rank-assignment loop in
TeamActivityService.assignRanks duplicates the logic in
RankingService.assignRanks; extract the common algorithm into a shared utility
(e.g., RankingUtils.assignRanks or a generic RankAssigner<T>) that accepts a
list of projections and a mapper function to produce memberId, handle and
solved-count, then return List<TeamActivityResponse.MemberRank> (or a generic
rank DTO) and call that utility from both TeamActivityService.assignRanks and
RankingService.assignRanks; reference MemberSolvedSummaryProjection,
GlobalRankingProjection, TeamActivityResponse.MemberRank and the assignRanks
methods to locate where to replace the duplicated loop with the shared generic
utility.
src/main/java/com/ryu/studyhelper/solve/repository/MemberSolvedProblemRepository.java (1)

25-26: 메서드 이름이 매우 깁니다 — @Query로 전환을 고려해 보세요.

findByMemberIdAndSolvedAtGreaterThanEqualAndSolvedAtLessThanOrderBySolvedAtAsc는 가독성이 낮습니다. 명시적 @Query 어노테이션과 짧은 메서드 이름(findByMemberIdAndSolvedAtRange 등)으로 대체하면 가독성이 크게 개선됩니다.

♻️ 제안
-    List<MemberSolvedProblem> findByMemberIdAndSolvedAtGreaterThanEqualAndSolvedAtLessThanOrderBySolvedAtAsc(
-            Long memberId, LocalDateTime start, LocalDateTime end);
+    `@Query`("SELECT msp FROM MemberSolvedProblem msp WHERE msp.member.id = :memberId AND msp.solvedAt >= :start AND msp.solvedAt < :end ORDER BY msp.solvedAt ASC")
+    List<MemberSolvedProblem> findByMemberIdAndSolvedAtRange(
+            `@Param`("memberId") Long memberId,
+            `@Param`("start") LocalDateTime start,
+            `@Param`("end") LocalDateTime end);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/ryu/studyhelper/solve/repository/MemberSolvedProblemRepository.java`
around lines 25 - 26, Replace the overly long derived query method
findByMemberIdAndSolvedAtGreaterThanEqualAndSolvedAtLessThanOrderBySolvedAtAsc
in the MemberSolvedProblemRepository with a concise method name (e.g.,
findByMemberIdAndSolvedAtRange) and annotate it with `@Query` containing the
equivalent JPQL (select m from MemberSolvedProblem m where m.member.id =
:memberId and m.solvedAt >= :start and m.solvedAt < :end order by m.solvedAt
asc); keep the parameter types (Long memberId, LocalDateTime start,
LocalDateTime end) and ensure you add the `@Param` annotations or matching
parameter names to the query so the repository still returns
List<MemberSolvedProblem>.
src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java (2)

218-250: 리포지토리 호출 시 any() 매처 사용으로 날짜 범위 파라미터 검증이 누락되어 있습니다.

findByMemberIdAndSolvedAtGreaterThanEqual... 호출에 any() 매처만 사용하고 있어, 실제로 올바른 날짜 범위가 전달되는지 검증하지 않습니다. 예를 들어 Line 230의 solved2는 15:00인데 Clock은 14:00으로 고정되어 있어 실제 서비스에서는 조회 범위 밖일 수 있습니다.

ArgumentCaptor를 사용해 전달되는 날짜 범위 파라미터도 검증하면 테스트 신뢰도가 높아집니다. 이 부분은 후속 개선 사항으로 고려해 주세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java` around
lines 218 - 250, The repository call in the test uses any() for the date range
so the test doesn't verify the actual time window passed into
memberSolvedProblemRepository.findByMemberIdAndSolvedAtGreaterThanEqualAndSolvedAtLessThanOrderBySolvedAtAsc;
add ArgumentCaptor<LocalDateTime> (or two captors) to capture the start and end
parameters when invoking solveServiceWithClock.getDailySolved(1L, 7) and assert
they match the expected range based on the fixed clock (e.g., start =
clock.now().minusDays(6).atStartOfDay, end =
clock.now().plusDays(1).atStartOfDay or equivalent used by the service), keeping
the mocked return values for solved1/solved2 but verifying that the later
solved2 (15:00) falls inside/outside the captured range as intended.

39-53: @InjectMocks로 생성된 solveServiceClock이 null로 주입됩니다.

SolveService 생성자는 Clock을 필요로 하지만, 외부 클래스 레벨에는 Clock에 대한 @Mock이 없습니다. Mockito @InjectMocks는 매칭되는 mock이 없으면 null을 주입합니다. 현재 VerifyProblemSolvedTest에서는 Clock을 사용하지 않아 동작하지만, 향후 solveServicegetDailySolved를 호출하면 NullPointerException이 발생합니다.

외부 클래스에도 Clock mock을 추가하거나, VerifyProblemSolvedTest처럼 명시적으로 SolveService를 생성하는 방식으로 통일하는 것을 권장합니다.

♻️ 제안: 외부 클래스에 Clock mock 추가
     `@Mock`
     private SolvedAcClient solvedAcClient;
 
+    `@Mock`
+    private Clock clock;
+
     private Member member;
     private Problem problem;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java` around
lines 39 - 53, The SolveService instance created with `@InjectMocks` is receiving
a null Clock because there is no Clock mock declared; add a `@Mock` Clock clock to
the test class (or alternatively stop using `@InjectMocks` and instantiate
SolveService explicitly like in VerifyProblemSolvedTest) so Mockito can inject a
non-null Clock into the SolveService constructor and avoid NullPointerException
when calling methods like getDailySolved; update references to SolveService,
`@InjectMocks`, Clock, and getDailySolved accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/main/java/com/ryu/studyhelper/solve/service/SolveService.java`:
- Around line 44-72: verifyProblemSolved has a TOCTOU race where concurrent
requests can both pass existsByMemberIdAndProblemId and the second save may
throw DataIntegrityViolationException due to the unique (member_id, problem_id)
constraint; wrap the memberSolvedProblemRepository.save(...) (after
MemberSolvedProblem.create(...)) in a try/catch that catches
DataIntegrityViolationException and throws new
CustomException(CustomResponseStatus.ALREADY_SOLVED) instead, keeping all other
behavior the same so callers receive ALREADY_SOLVED rather than a 500.

In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java`:
- Around line 203-216: The test uses ZoneId.systemDefault() which makes the
fixed time depend on the machine/CI timezone; update the setUp() to use a fixed
timezone (e.g., ZoneId.of("Asia/Seoul")) for both the ZonedDateTime creation and
the mocked clock.getZone() so the date-change-at-06:00 logic in the SolveService
test is deterministic; adjust the ZonedDateTime fixedTime and the
given(clock.getZone()).willReturn(...) to the same fixed ZoneId, keeping the
rest of the setUp() and SolveService construction unchanged.

---

Nitpick comments:
In
`@src/main/java/com/ryu/studyhelper/solve/repository/MemberSolvedProblemRepository.java`:
- Around line 25-26: Replace the overly long derived query method
findByMemberIdAndSolvedAtGreaterThanEqualAndSolvedAtLessThanOrderBySolvedAtAsc
in the MemberSolvedProblemRepository with a concise method name (e.g.,
findByMemberIdAndSolvedAtRange) and annotate it with `@Query` containing the
equivalent JPQL (select m from MemberSolvedProblem m where m.member.id =
:memberId and m.solvedAt >= :start and m.solvedAt < :end order by m.solvedAt
asc); keep the parameter types (Long memberId, LocalDateTime start,
LocalDateTime end) and ensure you add the `@Param` annotations or matching
parameter names to the query so the repository still returns
List<MemberSolvedProblem>.

In `@src/main/java/com/ryu/studyhelper/solve/service/SolveService.java`:
- Around line 81-135: Replace usages of member.getHandle().isEmpty() with
member.getHandle().isBlank() to also treat strings containing only whitespace as
empty; locate the check (currently using isEmpty) where member handles are
validated and update it to isBlank(), ensuring any logic that depends on an
empty handle (e.g., in getDailySolved-related user formatting or display flows)
continues to behave the same after the change.
- Around line 147-156: getAdjustedDate duplicates logic in
TeamActivityService.getMissionDate; extract the shared "06:00 기준 날짜 보정" into a
single utility method (e.g., MissionDateUtils.adjustToMissionDate(LocalDateTime)
or DateUtils.getMissionDate) and update both SolveService.getAdjustedDate and
TeamActivityService.getMissionDate to call that util (or remove the duplicate
method and replace callers with the util). Ensure the util accepts
LocalDateTime, performs the same hour < 6 -> minusDays(1) rule, and update
imports/usages so both SolveService and TeamActivityService delegate to that
common method.

In `@src/main/java/com/ryu/studyhelper/team/service/TeamActivityService.java`:
- Around line 154-181: The rank-assignment loop in
TeamActivityService.assignRanks duplicates the logic in
RankingService.assignRanks; extract the common algorithm into a shared utility
(e.g., RankingUtils.assignRanks or a generic RankAssigner<T>) that accepts a
list of projections and a mapper function to produce memberId, handle and
solved-count, then return List<TeamActivityResponse.MemberRank> (or a generic
rank DTO) and call that utility from both TeamActivityService.assignRanks and
RankingService.assignRanks; reference MemberSolvedSummaryProjection,
GlobalRankingProjection, TeamActivityResponse.MemberRank and the assignRanks
methods to locate where to replace the duplicated loop with the shared generic
utility.

In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java`:
- Around line 218-250: The repository call in the test uses any() for the date
range so the test doesn't verify the actual time window passed into
memberSolvedProblemRepository.findByMemberIdAndSolvedAtGreaterThanEqualAndSolvedAtLessThanOrderBySolvedAtAsc;
add ArgumentCaptor<LocalDateTime> (or two captors) to capture the start and end
parameters when invoking solveServiceWithClock.getDailySolved(1L, 7) and assert
they match the expected range based on the fixed clock (e.g., start =
clock.now().minusDays(6).atStartOfDay, end =
clock.now().plusDays(1).atStartOfDay or equivalent used by the service), keeping
the mocked return values for solved1/solved2 but verifying that the later
solved2 (15:00) falls inside/outside the captured range as intended.
- Around line 39-53: The SolveService instance created with `@InjectMocks` is
receiving a null Clock because there is no Clock mock declared; add a `@Mock`
Clock clock to the test class (or alternatively stop using `@InjectMocks` and
instantiate SolveService explicitly like in VerifyProblemSolvedTest) so Mockito
can inject a non-null Clock into the SolveService constructor and avoid
NullPointerException when calling methods like getDailySolved; update references
to SolveService, `@InjectMocks`, Clock, and getDailySolved accordingly.

Comment on lines 203 to 216
void setUp() {
// 2024-11-28 14:00 (오후 2시)으로 고정
ZonedDateTime fixedTime = ZonedDateTime.of(2024, 11, 28, 14, 0, 0, 0, ZoneId.systemDefault());
given(clock.instant()).willReturn(fixedTime.toInstant());
given(clock.getZone()).willReturn(ZoneId.systemDefault());

solveServiceWithClock = new SolveService(
memberRepository,
problemRepository,
memberSolvedProblemRepository,
solvedAcClient,
clock
);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ZoneId.systemDefault() 사용으로 테스트가 실행 환경의 타임존에 의존합니다.

오전 6시 기준 날짜 변경 로직을 테스트하는데 ZoneId.systemDefault()를 사용하면, CI 서버나 다른 개발자 환경의 타임존에 따라 테스트 결과가 달라질 수 있습니다. 고정된 타임존(예: Asia/Seoul)을 사용하는 것이 테스트 안정성을 높입니다.

♻️ 고정 타임존 사용 제안
+        private static final ZoneId ZONE_KST = ZoneId.of("Asia/Seoul");
+
         `@BeforeEach`
         void setUp() {
-            ZonedDateTime fixedTime = ZonedDateTime.of(2024, 11, 28, 14, 0, 0, 0, ZoneId.systemDefault());
+            ZonedDateTime fixedTime = ZonedDateTime.of(2024, 11, 28, 14, 0, 0, 0, ZONE_KST);
             given(clock.instant()).willReturn(fixedTime.toInstant());
-            given(clock.getZone()).willReturn(ZoneId.systemDefault());
+            given(clock.getZone()).willReturn(ZONE_KST);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
void setUp() {
// 2024-11-28 14:00 (오후 2시)으로 고정
ZonedDateTime fixedTime = ZonedDateTime.of(2024, 11, 28, 14, 0, 0, 0, ZoneId.systemDefault());
given(clock.instant()).willReturn(fixedTime.toInstant());
given(clock.getZone()).willReturn(ZoneId.systemDefault());
solveServiceWithClock = new SolveService(
memberRepository,
problemRepository,
memberSolvedProblemRepository,
solvedAcClient,
clock
);
}
private static final ZoneId ZONE_KST = ZoneId.of("Asia/Seoul");
`@BeforeEach`
void setUp() {
// 2024-11-28 14:00 (오후 2시)으로 고정
ZonedDateTime fixedTime = ZonedDateTime.of(2024, 11, 28, 14, 0, 0, 0, ZONE_KST);
given(clock.instant()).willReturn(fixedTime.toInstant());
given(clock.getZone()).willReturn(ZONE_KST);
solveServiceWithClock = new SolveService(
memberRepository,
problemRepository,
memberSolvedProblemRepository,
solvedAcClient,
clock
);
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java` around
lines 203 - 216, The test uses ZoneId.systemDefault() which makes the fixed time
depend on the machine/CI timezone; update the setUp() to use a fixed timezone
(e.g., ZoneId.of("Asia/Seoul")) for both the ZonedDateTime creation and the
mocked clock.getZone() so the date-change-at-06:00 logic in the SolveService
test is deterministic; adjust the ZonedDateTime fixedTime and the
given(clock.getZone()).willReturn(...) to the same fixed ZoneId, keeping the
rest of the setUp() and SolveService construction unchanged.

- verifyProblemSolved에서 DataIntegrityViolationException → ALREADY_SOLVED 변환
- 테스트 ZoneId.systemDefault() → ZoneId.of("Asia/Seoul")로 고정
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java`:
- Around line 76-191: Add a TOCTOU concurrency test to SolveServiceTest that
simulates save() throwing DataIntegrityViolationException and asserts it is
translated to ALREADY_SOLVED: in the VerifyProblemSolvedTest nested class add a
test (e.g., fail_concurrentDuplicate) that stubs memberRepository.findById(1L)
-> member, problemRepository.findById(1000L) -> problem,
memberSolvedProblemRepository.existsByMemberIdAndProblemId(1L,1000L) -> false,
solvedAcClient.hasUserSolvedProblem("testuser",1000L) -> true, and
memberSolvedProblemRepository.save(any(MemberSolvedProblem.class)) to
willThrow(new DataIntegrityViolationException("duplicate")); then assert that
calling solveService.verifyProblemSolved(1L,1000L) throws CustomException with
status CustomResponseStatus.ALREADY_SOLVED.
- Around line 39-53: The test is missing a Clock mock injection and a TOCTOU
test for the DataIntegrityViolationException path; update SolveServiceTest to
either provide a mocked Clock to the `@InjectMocks` SolveService (or construct
SolveService explicitly with a mocked Clock) so Mockito does not inject null,
and add a unit test for verifyProblemSolved that stubs
memberSolvedProblemRepository.save (or the exact persistence call used in
verifyProblemSolved) to throw a DataIntegrityViolationException and assert that
verifyProblemSolved translates it into the ALREADY_SOLVED exception; reference
SolveService, verifyProblemSolved, Clock, memberSolvedProblemRepository,
DataIntegrityViolationException and ALREADY_SOLVED when locating where to add
the mock and the new test.

- 외부 클래스에 Clock mock 필드 추가
- 동시 요청 중복 저장 시 ALREADY_SOLVED 변환 테스트 추가
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java (1)

217-218: 내부 클래스의 Clock mock이 외부 클래스 필드를 섀도잉합니다.

GetDailySolvedTest@Mock Clock clock이 외부 클래스(line 54-55)의 동일한 필드명을 숨기고 있습니다. Mockito가 @Nested 클래스의 mock을 별도로 초기화하므로 동작에는 문제없지만, 필드명을 testClock 등으로 변경하면 가독성이 향상됩니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java` around
lines 217 - 218, The nested test class GetDailySolvedTest declares a `@Mock` Clock
clock that shadows the outer class's Clock field; rename the nested mock (e.g.,
to testClock) and update all references inside GetDailySolvedTest to use that
new name so there is no field shadowing; modify the `@Mock` declaration in
GetDailySolvedTest, and update any setup/when/verify calls inside
GetDailySolvedTest that currently reference clock to use testClock (leave the
outer Clock field and its usages untouched).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/test/java/com/ryu/studyhelper/solve/service/SolveServiceTest.java`:
- Around line 217-218: The nested test class GetDailySolvedTest declares a `@Mock`
Clock clock that shadows the outer class's Clock field; rename the nested mock
(e.g., to testClock) and update all references inside GetDailySolvedTest to use
that new name so there is no field shadowing; modify the `@Mock` declaration in
GetDailySolvedTest, and update any setup/when/verify calls inside
GetDailySolvedTest that currently reference clock to use testClock (leave the
outer Clock field and its usages untouched).

@ryuwldnjs ryuwldnjs merged commit 09b1915 into main Feb 17, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[REFACTOR] solve 도메인 신설 — 풀이 인증/기록/랭킹 분리

1 participant